//////////////////////////////////////////////////////////////////////////
//
// PROJECT:     Small and fast HTTP server
// FILE:        HTTPServerSession.cpp
// CREATED:     25-Dec-2013
// AUTHOR:      Vladimir Gumenuk
// DESCRIPTION: 
// REVISION:    0.0.0.1
//
//////////////////////////////////////////////////////////////////////////

#include "StdAfx.h"
#include "HTTPServerSession.h"

#pragma comment(lib, "httpapi.lib")

CHTTPServerSession::CHTTPServerSession()
{
    m_httpSessionID = 0;
    m_hUrlGroupID   = 0;
    m_hRequestQueue = NULL;
}

CHTTPServerSession::~CHTTPServerSession()
{
}

HRESULT CHTTPServerSession::Initialize()
{
    HRESULT hr = E_FAIL;
    
    HTTPAPI_VERSION HttpApiVersion = HTTPAPI_VERSION_2;
    DWORD dwRes = HttpInitialize( HttpApiVersion, HTTP_INITIALIZE_SERVER, 0 );
    if( dwRes != NO_ERROR )
        return HRESULT_FROM_WIN32( dwRes );

    dwRes = HttpCreateServerSession( HttpApiVersion, &m_httpSessionID, 0 );
    if( dwRes == NO_ERROR )
    {
        dwRes = HttpCreateRequestQueue( HttpApiVersion, NULL, NULL, 0, &m_hRequestQueue );
        if( dwRes == NO_ERROR )
        {
            dwRes = HttpCreateUrlGroup( m_httpSessionID, &m_hUrlGroupID, 0 );
            if( dwRes == NO_ERROR )
            {
                dwRes = HttpAddUrlToUrlGroup( m_hUrlGroupID, L"http://localhost:80/testserver/", 0, 0 );
                if( dwRes == NO_ERROR )
                {
                    // TODO: read some config file and add any other URL endpoints to this group

                    // bind URL group with request queue
                    HTTP_BINDING_INFO bindInfo;
                    bindInfo.RequestQueueHandle = m_hRequestQueue;
                    bindInfo.Flags.Present = 1;
                    dwRes = HttpSetUrlGroupProperty( m_hUrlGroupID, HttpServerBindingProperty, &bindInfo, sizeof(bindInfo) );
                    
                }
            }
        }
    }

    if( dwRes == NO_ERROR )
        hr = S_OK;
    else
        hr = HRESULT_FROM_WIN32( dwRes );

    return hr;
}

HRESULT CHTTPServerSession::Uninitialize()
{
    if( IsRunning() )
        return E_UNEXPECTED;

    DWORD dwRes;

    if( m_hUrlGroupID )
    {
        dwRes = HttpCloseUrlGroup( m_hUrlGroupID );
        m_hUrlGroupID = 0;
    }

    if( m_hRequestQueue )
    {
        dwRes = HttpCloseRequestQueue( m_hRequestQueue );
        m_hRequestQueue = 0;
    }
        
    if( m_httpSessionID )
    {
        dwRes = HttpCloseServerSession( m_httpSessionID );
        dwRes = HttpTerminate( HTTP_INITIALIZE_SERVER, 0 );
        m_httpSessionID = 0;
    }

    return S_OK;
}

HRESULT CHTTPServerSession::Start()
{
    if( !m_httpSessionID || !m_hRequestQueue || !m_hUrlGroupID )
        return E_UNEXPECTED;

    return StartThread();
}

HRESULT CHTTPServerSession::Stop()
{
    if( IsRunning() )
    {
        DWORD dwRes = HttpShutdownRequestQueue( m_hRequestQueue ); // cancel waiting for requests...

        StopThread();
    }

    return S_OK;
}

void CHTTPServerSession::Run()
{
    DoReceiveRequests();
}

DWORD CHTTPServerSession::DoReceiveRequests()
{
    ULONG              result;
    HTTP_REQUEST_ID    requestId;
    DWORD              bytesRead;
    PHTTP_REQUEST      pRequest;
    PCHAR              pRequestBuffer;
    ULONG              RequestBufferLength;

    // Allocate a 2 KB buffer. This size should work for most 
    // requests. The buffer size can be increased if required. Space
    // is also required for an HTTP_REQUEST structure.
    RequestBufferLength = sizeof(HTTP_REQUEST) + 2048;
    pRequestBuffer      = (PCHAR) new CHAR[ RequestBufferLength ];

    if( pRequestBuffer == NULL )
    {
        return ERROR_NOT_ENOUGH_MEMORY;
    }

    pRequest = (PHTTP_REQUEST)pRequestBuffer;

    // Wait for a new request. This is indicated by a NULL request ID.
    HTTP_SET_NULL_ID( &requestId );

    while( TRUE )
    {
        ZeroMemory( pRequest, RequestBufferLength );

        result = HttpReceiveHttpRequest( m_hRequestQueue, requestId, 0, pRequest, RequestBufferLength, &bytesRead, NULL );
    
        if( NO_ERROR == result )
        {
            switch(pRequest->Verb)
            {
            case HttpVerbGET:
                wprintf( L"Got a GET request for %ws \n", pRequest->CookedUrl.pFullUrl );
                result = SendHttpResponse( pRequest, 200, "OK", "Hey! You hit the server \r\n" );
                break;

            case HttpVerbPOST:
                wprintf(L"Got a POST request for %ws \n", pRequest->CookedUrl.pFullUrl );
                result= SendHttpPostResponse( pRequest );
                break;

            default:
                wprintf( L"Got a unknown request for %ws \n", pRequest->CookedUrl.pFullUrl );
                result = SendHttpResponse( pRequest, 503, "Not Implemented", NULL );
                break;
            }

            if(result != NO_ERROR)
            {
                break;
            }

            //
            // Reset the Request ID to handle the next request.
            //
            HTTP_SET_NULL_ID( &requestId );
        }
        else if(result == ERROR_MORE_DATA)
        {
            //
            // The input buffer was too small to hold the request
            // headers. Increase the buffer size and call the 
            // API again. 
            //
            // When calling the API again, handle the request
            // that failed by passing a RequestID.
            //
            // This RequestID is read from the old buffer.
            //
            requestId = pRequest->RequestId;

            // Free the old buffer and allocate a new buffer.
            RequestBufferLength = bytesRead;
            delete []pRequestBuffer;
            pRequestBuffer = (PCHAR) new CHAR[ RequestBufferLength ];
            if (pRequestBuffer == NULL)
            {
                result = ERROR_NOT_ENOUGH_MEMORY;
                break;
            }

            pRequest = (PHTTP_REQUEST)pRequestBuffer;
        }
        else if(ERROR_CONNECTION_INVALID == result && !HTTP_IS_NULL_ID(&requestId) )
        {
            // The TCP connection was corrupted by the peer when attempting to handle a request with more buffer. 
            // Continue to the next request.
            HTTP_SET_NULL_ID( &requestId );
        }
        else
        {
            break;
        }

    }

    if( pRequestBuffer )
    {
        delete []pRequestBuffer;
    }

    return result;
}

DWORD CHTTPServerSession::SendHttpResponse( IN PHTTP_REQUEST pRequest, IN USHORT StatusCode, IN PSTR pReason, IN PSTR pEntityString )
{
    HTTP_RESPONSE   response;
    HTTP_DATA_CHUNK dataChunk;
    DWORD           result;
    DWORD           bytesSent;

    // Initialize the HTTP response structure.
    INITIALIZE_HTTP_RESPONSE( &response, StatusCode, pReason );

    // Add a known header.
    ADD_KNOWN_HEADER( response, HttpHeaderContentType, "text/html" );
   
    if( pEntityString )
    {
        // Add an entity chunk.
        dataChunk.DataChunkType           = HttpDataChunkFromMemory;
        dataChunk.FromMemory.pBuffer      = pEntityString;
        dataChunk.FromMemory.BufferLength = (ULONG) strlen(pEntityString);

        response.EntityChunkCount         = 1;
        response.pEntityChunks            = &dataChunk;
    }

    // Because the entity body is sent in one call, it is not required to specify the Content-Length.
    result = HttpSendHttpResponse( m_hRequestQueue, pRequest->RequestId, 0, &response, NULL, &bytesSent, NULL, 0, NULL, NULL ); 
    if( result != NO_ERROR )
    {
        wprintf(L"HttpSendHttpResponse failed with %lu \n", result);
    }

    return result;
}

#define MAX_ULONG_STR ((ULONG) sizeof("4294967295"))

DWORD CHTTPServerSession::SendHttpPostResponse( IN PHTTP_REQUEST pRequest )
{
    HTTP_RESPONSE   response;
    DWORD           result;
    DWORD           bytesSent;
    PUCHAR          pEntityBuffer;
    ULONG           EntityBufferLength;
    ULONG           BytesRead;
    ULONG           TempFileBytesWritten;
    HANDLE          hTempFile;
    TCHAR           szTempName[MAX_PATH + 1];
    CHAR            szContentLength[MAX_ULONG_STR];
    HTTP_DATA_CHUNK dataChunk;
    ULONG           TotalBytesRead = 0;

    BytesRead  = 0;
    hTempFile  = INVALID_HANDLE_VALUE;

    // Allocate space for an entity buffer. Buffer can be increased on demand.
    EntityBufferLength = 2048;
    pEntityBuffer      = (PUCHAR) new UCHAR[ EntityBufferLength ];
    if( pEntityBuffer == NULL )
    {
        result = ERROR_NOT_ENOUGH_MEMORY;
        wprintf(L"Insufficient resources \n");
        goto Done;
    }

    // Initialize the HTTP response structure.
    INITIALIZE_HTTP_RESPONSE(&response, 200, "OK");

    //
    // For POST, echo back the entity from the
    // client
    //
    // NOTE: If the HTTP_RECEIVE_REQUEST_FLAG_COPY_BODY flag had been
    //       passed with HttpReceiveHttpRequest(), the entity would 
    //       have been a part of HTTP_REQUEST (using the pEntityChunks
    //       field). Because that flag was not passed, there are no
    //       o entity bodies in HTTP_REQUEST.
    if(pRequest->Flags & HTTP_REQUEST_FLAG_MORE_ENTITY_BODY_EXISTS)
    {
        // The entity body is sent over multiple calls. Collect 
        // these in a file and send back. Create a temporary 
        // file.

        if(GetTempFileName( L".", L"New", 0, szTempName ) == 0 )
        {
            result = GetLastError();
            wprintf(L"GetTempFileName failed with %lu \n", result);
            goto Done;
        }

        hTempFile = CreateFile( szTempName, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );

        if(hTempFile == INVALID_HANDLE_VALUE)
        {
            result = GetLastError();
            wprintf(L"Cannot create temporary file. Error %lu \n",
                     result);
            goto Done;
        }

        do
        {
            //
            // Read the entity chunk from the request.
            //
            BytesRead = 0; 
            result = HttpReceiveRequestEntityBody( m_hRequestQueue, pRequest->RequestId, 0, pEntityBuffer, EntityBufferLength, &BytesRead, NULL );

            switch(result)
            {
                case NO_ERROR:

                    if(BytesRead != 0)
                    {
                        TotalBytesRead += BytesRead;
                        WriteFile( hTempFile,  pEntityBuffer, BytesRead, &TempFileBytesWritten, NULL );
                    }
                    break;

                case ERROR_HANDLE_EOF:

                    //
                    // The last request entity body has been read.
                    // Send back a response. 
                    //
                    // To illustrate entity sends via 
                    // HttpSendResponseEntityBody, the response will 
                    // be sent over multiple calls. To do this,
                    // pass the HTTP_SEND_RESPONSE_FLAG_MORE_DATA
                    // flag.
                    
                    if(BytesRead != 0)
                    {
                        TotalBytesRead += BytesRead;
                        WriteFile( hTempFile,  pEntityBuffer, BytesRead, &TempFileBytesWritten, NULL );
                    }

                    //
                    // Because the response is sent over multiple
                    // API calls, add a content-length.
                    //
                    // Alternatively, the response could have been
                    // sent using chunked transfer encoding, by  
                    // passing "Transfer-Encoding: Chunked".
                    //

                    // NOTE: Because the TotalBytesread in a ULONG
                    //       are accumulated, this will not work
                    //       for entity bodies larger than 4 GB. 
                    //       For support of large entity bodies,
                    //       use a ULONGLONG.
                    // 
                                     
                    sprintf_s(szContentLength, MAX_ULONG_STR, "%lu", TotalBytesRead);

                    ADD_KNOWN_HEADER( response,  HttpHeaderContentLength, szContentLength );

                    result = HttpSendHttpResponse(
                               m_hRequestQueue,           // ReqQueueHandle
                               pRequest->RequestId, // Request ID
                               HTTP_SEND_RESPONSE_FLAG_MORE_DATA,
                               &response,       // HTTP response
                               NULL,            // pReserved1
                               &bytesSent,      // bytes sent-optional
                               NULL,            // pReserved2
                               0,               // Reserved3
                               NULL,            // LPOVERLAPPED
                               NULL             // pReserved4
                               );

                    if(result != NO_ERROR)
                    {
                        wprintf( L"HttpSendHttpResponse failed with %lu \n", result );
                        goto Done;
                    }

                    //
                    // Send entity body from a file handle.
                    //
                    dataChunk.DataChunkType = HttpDataChunkFromFileHandle;
                    dataChunk.FromFileHandle.ByteRange.StartingOffset.QuadPart = 0;
                    dataChunk.FromFileHandle.ByteRange.Length.QuadPart = HTTP_BYTE_RANGE_TO_EOF;
                    dataChunk.FromFileHandle.FileHandle = hTempFile;

                    result = HttpSendResponseEntityBody(
                                m_hRequestQueue,
                                pRequest->RequestId,
                                0,           // This is the last send.
                                1,           // Entity Chunk Count.
                                &dataChunk,
                                NULL,
                                NULL,
                                0,
                                NULL,
                                NULL
                                );

                    if(result != NO_ERROR)
                    {
                       wprintf( L"HttpSendResponseEntityBody failed %lu\n", result );
                    }

                    goto Done;

                    break;
                       

                default:
                  wprintf( L"HttpReceiveRequestEntityBody failed with %lu \n", result);
                  goto Done;
            }

        } while(TRUE);
    }
    else
    {
        // This request does not have an entity body.
        result = HttpSendHttpResponse(
                   m_hRequestQueue,           // ReqQueueHandle
                   pRequest->RequestId, // Request ID
                   0,
                   &response,           // HTTP response
                   NULL,                // pReserved1
                   &bytesSent,          // bytes sent (optional)
                   NULL,                // pReserved2
                   0,                   // Reserved3
                   NULL,                // LPOVERLAPPED
                   NULL                 // pReserved4
                   );
        if(result != NO_ERROR)
        {
            wprintf(L"HttpSendHttpResponse failed with %lu \n", result );
        }
    }

Done:
    if( pEntityBuffer )
    {
        delete []pEntityBuffer;
    }

    if(INVALID_HANDLE_VALUE != hTempFile)
    {
        CloseHandle(hTempFile);
        DeleteFile(szTempName);
    }

    return result;
}
